package com.pspaceplus.driver.bacnetdemo.init;

import com.pspaceplus.driver.bacnetdemo.util.BACnetUtil;
import com.pspaceplus.driver.bacnetdemo.util.IpUtil;
import com.serotonin.bacnet4j.LocalDevice;
import com.serotonin.bacnet4j.RemoteDevice;
import com.serotonin.bacnet4j.exception.BACnetException;
import com.serotonin.bacnet4j.npdu.ip.IpNetwork;
import com.serotonin.bacnet4j.type.Encodable;
import com.serotonin.bacnet4j.type.enumerated.PropertyIdentifier;
import com.serotonin.bacnet4j.type.primitive.CharacterString;
import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier;
import com.serotonin.bacnet4j.util.*;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @ClassName: run
 * @Description: TODO-wcy
 * @Author: wcy
 * @Date: 2022/4/25 16:01
 * @Version: 1.0
 */
@Component
@Order(0)
public class TestBACnet implements ApplicationRunner {

    LocalDevice localDevice;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // sdk源码github地址：https://github.com/MangoAutomation/BACnet4J/
        // 本代码参考内容：https://blog.csdn.net/dream_broken/article/details/106646604

        // 重要：
        // 0.此代码基于Yabe这个可视化工具及其附带的模拟器进行编写。Yabe下载地址：https://liquidtelecom.dl.sourceforge.net/project/yetanotherbacnetexplorer/SetupYabe_v1.2.2.exe
        //   Yabe安装后将自动带有模拟器 Bacnet.Room.Simulator.exe。（模拟器文件路径：模拟器在Yabe软件安装路径的/AddOn文件夹下。我的文件路径为：D:\ProgramFiles\Work\Others\BACnet\Yabe\AddOn\Bacnet.Room.Simulator.lnk）
        // 1.运行此代码前：需在本机或者与本机同网段的其他机器上，运行Yabe软件附带的 Bacnet.Room.Simulator.exe ，代码里将访问此设备
        //   tips：该模拟器支持多开（多次打开此模拟器，会启动多个设备id不同的设备）
        // 2.运行此代码前：需关闭 Yabe、InneaBACnetExplorer Free Edition等所有的浏览设备信息的可视化工具，否则代码里将无法请求到设备（当初只关了Yabe忘记了曾经安装过的InneaBACnetExplorer Free Edition，导致浪费了半天多的时间QAQ）
        // 3.若要运行TestBACnetForSubscribe.java，则需将其内的 int remoteDeviceIdForSubscribeCOV = 1425616; 修改为你实际要访问的设备id值（Bacnet.Room.Simulator模拟器的设备id）

//        String ip = "172.19.83.237";
//        String broadcast = "172.19.83.255";
//        int remoteDeviceNumber = 260002;

        String ip = IpUtil.getIpAddress(); // 本机ip
        String subnet = IpUtil.getSubnet(); // 本机子网掩码
        IpNetwork ipNetwork = BACnetUtil.initIpNetwork(ip, subnet);

        // 初始化一个虚拟的本地设备，用于发送请求
        int localDeviceNumber = 6451; // 任意写，和已有设备id重复也没关系
        localDevice = BACnetUtil.initLocalDevice(ipNetwork, localDeviceNumber);

        // 通过udp广播，获取本网段内的所有设备
        List<RemoteDevice> remoteDevices = getAllRemoteDevices(IpUtil.getBroadcastByIpAndSubnet(ip, subnet));

        // 通过udp广播，获取指定设备
        int remoteDeviceNumber = remoteDevices.get(0).getInstanceNumber(); // 需要是一个有效的设备id，为了避免其他人运行修改代码时麻烦，我直接取设备列表的第一个值，保证是有效id
        RemoteDevice remoteDeviceTemp = getSingleRemoteDevice(remoteDeviceNumber);

        // 获取设备的对象列表
        List<ObjectIdentifier> objectListOfDevice = getObjectListOfDevice(remoteDeviceTemp);

        ObjectIdentifier objectIdentifier = objectListOfDevice.get(0); // 取第一个对象
        // 获取对象的属性列表
        /*
         说明：协议中说，标准对象类型有54种，并且协议为每种对象类型列出了属性列表，指明了哪些属性是可选属性，哪些属性是必选属性。
              同时，协议中还说，是允许在定义的标准对象类型及属性列表外添加非标准的对象类型或属性的（不过这个应该是对生产厂家说的，允许生产厂家这么操作）。
        */

        // 读取属性的值
        Map<PropertyIdentifier, Encodable> propertyIdentifierEncodableMap = readPropertyValue(remoteDeviceTemp, objectIdentifier);

        // 写属性的值（属性暂时只写presentValue，写其他属性的值会报错访问被拒绝）
        // a.暂时只测试字符串类型，先筛选出类型是字符串的对象
        List<ObjectIdentifier> characterstringList = objectListOfDevice.stream().filter(t -> t.getObjectType().toString().equals("characterstring-value")).collect(Collectors.toList());
        ObjectIdentifier objectIdentifierWithCharacterStringType = characterstringList.get(0);

        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String currentTimeStr = dateTimeFormatter.format(LocalDateTime.now());
        // b.构造要写入的值
        CharacterString valueToWrite = new CharacterString("pSpaceplus " + currentTimeStr);

        Encodable writedValue = writePropertyValue(remoteDeviceTemp, objectIdentifierWithCharacterStringType, PropertyIdentifier.presentValue, valueToWrite);

        // 终止本地虚拟设备
        localDevice.terminate();
        System.out.println("本次测试终止");
        System.out.println();
        System.out.println();
    }

    private Encodable writePropertyValue(RemoteDevice remoteDevice, ObjectIdentifier objectIdentifier, PropertyIdentifier propertyIdentifier, Encodable valueToWrite) throws BACnetException, InterruptedException {
        System.out.println("************************** 修改对象中属性的值 对象id: " + objectIdentifier.getInstanceNumber() + ", 属性名: " + propertyIdentifier.toString() + " ****************************");

        Encodable propertyValueBeforeWrite = RequestUtils.getProperty(localDevice, remoteDevice, objectIdentifier, propertyIdentifier);
        System.out.println("propertyValueBeforeWrite = " + propertyValueBeforeWrite);
        /*// 原文备注说：必须先修改out of service为true，但实测修改时报错：write access denied
        RequestUtils.writeProperty(localDevice,
                remoteDevice,
                objectIdentifier,
                PropertyIdentifier.outOfService,
                Boolean.TRUE); // com.serotonin.bacnet4j.type.primitive.Boolean
        Thread.sleep(1000);*/
        //修改属性值
        RequestUtils.writeProperty(localDevice,
                remoteDevice,
                objectIdentifier,
                propertyIdentifier,
                valueToWrite); // com.serotonin.bacnet4j.type.primitive.Double
//        Thread.sleep(2000);
        Encodable propertyValueAfterWrite = RequestUtils.getProperty(localDevice, remoteDevice, objectIdentifier, propertyIdentifier);
        System.out.println("propertyValueAfterWrite  = " + propertyValueAfterWrite);

        return propertyValueAfterWrite;
    }

    private Map<PropertyIdentifier, Encodable> readPropertyValue(RemoteDevice remoteDevice, ObjectIdentifier objectIdentifier) throws BACnetException {
        System.out.println("************************** 获取对象中属性的值 " + objectIdentifier.getInstanceNumber() + " ****************************");
        PropertyIdentifier[] propertyIdentifiers = new PropertyIdentifier[]{
                PropertyIdentifier.objectName,
                PropertyIdentifier.description,
                // presentValue是该对象当前的值，其它属性都是描述这个对象的
                PropertyIdentifier.presentValue,
                PropertyIdentifier.eventState,
                PropertyIdentifier.outOfService,
                PropertyIdentifier.statusFlags
        };
        // 批量查询该对象的属性值
        /*
        * 三种方法的返回值对比：
        * 方法三是返回: key: 属性id, value: 属性值；
        * 方法二是返回: key: 对象id-属性id, value: 属性值；
        * 方法一是返回: key: 设备id, value: (key: 对象id-属性id, value: 属性值)
        */
        // 方法一：PropertyUtils.readProperties，传入的是设备的id，而不是已经获取到的remoteDevice对象，返回的是 DeviceObjectPropertyValues
        DeviceObjectPropertyReferences deviceObjectPropertyReferences = new DeviceObjectPropertyReferences();
        deviceObjectPropertyReferences.add(remoteDevice.getInstanceNumber(), objectIdentifier, propertyIdentifiers);
        DeviceObjectPropertyValues deviceObjectPropertyValues = PropertyUtils.readProperties(localDevice, deviceObjectPropertyReferences, null);
        // 方法二：RequestUtils.readProperties，传入的是已经获取到的remoteDevice对象，可以指定是否允许为null，返回的是 PropertyValues
        PropertyReferences propertyReferences = new PropertyReferences();
        propertyReferences.add(objectIdentifier, propertyIdentifiers);
        PropertyValues objectPropertyReferences = RequestUtils.readProperties(localDevice, remoteDevice, propertyReferences, true, null);
        // 方法三：RequestUtils.getProperties， 传入的是已经获取到的remoteDevice对象，不可以指定是否允许为null，返回的是 Map<PropertyIdentifier, Encodable>
        Map<PropertyIdentifier, Encodable> properties = RequestUtils.getProperties(localDevice, remoteDevice, objectIdentifier, null, propertyIdentifiers);

        // 单独查询指定的属性值
        Encodable propertyEncodableValue = RequestUtils.getProperty(localDevice, remoteDevice, objectIdentifier, PropertyIdentifier.presentValue);

        properties.forEach((propertyIdentifier, value) -> {
            System.out.println(propertyIdentifier.toString() + ": " + value.toString());
        });

        return properties;
    }

    private List<ObjectIdentifier> getObjectListOfDevice(RemoteDevice remoteDevice) throws BACnetException {
        System.out.println("************************** 获取设备下的对象列表 " + remoteDevice.getInstanceNumber() + " ****************************");
        List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice, remoteDevice).getValues();

        // 列表中会将设备本身也返回回来，先排除掉吧，毕竟它不是设备下的对象
        List<ObjectIdentifier> realObjectList = objectList.stream().filter(t -> !t.getObjectType().toString().equals("device")).collect(Collectors.toList());

        realObjectList.forEach(object -> {
            try {
                Encodable objectName = RequestUtils.readProperty(localDevice,
                        remoteDevice,
                        new ObjectIdentifier(object.getObjectType(), object.getInstanceNumber()),
                        PropertyIdentifier.objectName,
                        null);
                System.out.println("instanceNumber: " + object.getInstanceNumber() + ", objectType: " + object.getObjectType() + ", objectName: " + objectName);
            } catch (BACnetException e) {
                e.printStackTrace();
            }
        });
        return realObjectList;
    }

    private RemoteDevice getSingleRemoteDevice(int remoteDeviceNumber) throws BACnetException {
        System.out.println("************************** 获取指定的设备 " + remoteDeviceNumber + " ****************************");
        // 还有另一种方法获取指定设备：localDevice.getRemoteDeviceBlocking(remoteDeviceNumber)
        RemoteDevice remoteDeviceTemp = RemoteDeviceFinder.findDevice(localDevice, remoteDeviceNumber).get();
        // 下一行的方法为jar包提供的工具方法，所做的内容就是通过 localDevice.send 获取属性值并赋值到remoteDevice上，以便直接通过remoteDevice.get...的方式调用
        // Tips: 最好保留此方法，因为后续 RequestUtils.readProperty 等方法会使用到此工具方法中对 remoteDevice 这个对象上所赋的值（注释此行则可以看到NullPointerException的报错）
        DiscoveryUtils.getExtendedDeviceInformation(localDevice, remoteDeviceTemp);

        System.out.println("remoteDeviceTemp.getName() = " + remoteDeviceTemp.getName());
        System.out.println("remoteDeviceTemp.getModelName() = " + remoteDeviceTemp.getModelName());
        System.out.println("objectType: " + remoteDeviceTemp.getObjectIdentifier().getObjectType().toString() + ", instanceNumber: " + remoteDeviceTemp.getObjectIdentifier().getInstanceNumber());
        return remoteDeviceTemp;
    }

    private List<RemoteDevice> getAllRemoteDevices(String networkSegment) throws InterruptedException {
        System.out.println("************************** 获取本网段所有的设备 " + networkSegment + " ****************************");
        RemoteDeviceDiscoverer remoteDeviceDiscoverer = new RemoteDeviceDiscoverer(localDevice);
        remoteDeviceDiscoverer.start();

        // 等待网段内的设备响应广播消息
        Thread.sleep(3000);

        List<RemoteDevice> remoteDevices = remoteDeviceDiscoverer.getRemoteDevices();
        remoteDevices.forEach(remoteDevice -> {
            try {
                // 下一行的方法为jar包提供的工具方法，所做的内容就是通过 localDevice.send 获取属性值并赋值到remoteDevice上，以便直接通过remoteDevice.get...的方式调用
                DiscoveryUtils.getExtendedDeviceInformation(localDevice, remoteDevice);
            } catch (BACnetException e) {
                e.printStackTrace();
            }

            System.out.println("device " + remoteDevice.getObjectIdentifier().getInstanceNumber() + ":");
            System.out.println("    remoteDeviceTemp.getName() = " + remoteDevice.getName());
            System.out.println("    remoteDeviceTemp.getModelName() = " + remoteDevice.getModelName());
            System.out.println("    objectType: " + remoteDevice.getObjectIdentifier().getObjectType().toString() + ", instanceNumber: " + remoteDevice.getObjectIdentifier().getInstanceNumber());
        });
        remoteDeviceDiscoverer.stop();
        return remoteDevices;
    }
}
